Mise à jour le 13/11/2021
PHP RFC: Class Friendship

PHP RFC: Class Friendship

1. Présentation de la RFC

Date: 2017-09-21
Url de la RFC : https://wiki.php.net/rfc/friend-classes

Principe de base :
on introduit le mot clef "friend" qui permet à une classe d'avoir accès aux propriétés protected et privée d'une autre classe pour éventuellement séparer les règles d'affichages des paramètres de contenu.

Clairement, je comprend pourquoi cette RFC a été refusée en grande majorité par la communauté (6 votes POUR / 27 votes CONTRE).
Alors déjà que l'utilisation des Traits en PHP me semble être une grosse ruse pour palier à l'héritage multiple, alors ce principe de Friend me semble apporter plus de complexité dans le code que la souplesse que cela entend.
La goutte de trop est l'exemple du FibonacciTest : on commence à mettre une dépendance avec les classes de test DANS le code de l'application.
Techniquement, on sent bien que le dossier src/test ou équivalent ne doit pas partir en l'état en production, le code source qui tourne en production doit être le plus light possible et ne pas embarquer des choses qui ne s'y executeront jamais : d'une part c'est plus "écolo" car ça évite de transporter des milliers de fichiers pour rien sur les serveurs de production mais en plus si quelqu'un arrivait à executer des fichiers de test à distance, rien ne dit qu'il executerait pas un programme qui effacerait la base de données pour charger son jeu de test.

Donc non.

La réponse la plus satisfaisante pour lire des attributs publiques reste de les rendre accessibles par des getters soit généraux, soit spécifiques dans le cas où on souhaiterait qu'une et une seule classe puisse avoir accès aux données.

2. Exemple sans l'usage de friend via des getters classiques

Eventuellement, ça pourrait ressembler à ceci (note : j'ai enlevé la dépendance avec la classe Uuid, non pertinente) :

NoFriend.php
<?php

class Person
{
    protected $id;
    protected $firstName;
    protected $lastName;

    public function __construct($id, $firstName, $lastName)
    {
        $this->id = $id;
        $this->firstName = $firstName;
        $this->lastName = $lastName;
    }

    public function getId()
    {
        return $this->id;
    }

    public function getFirstName()
    {
        return $this->firstName;
    }

    public function getLastName()
    {
        return $this->lastName;
    }
}

class HumanResourceReport
{
    private $person;

    public function __construct(Person $person)
    {
        $this->person = $person;
    }

    public function getFullName()
    {
        return $this->person->getFirstName() . ' ' . $this->person->getLastName();
    }

    public function getReportIdentifier()
    {
        return "HR_REPORT_ID_{$this->person->getId()}";
    }
}

$person = new Person('uniq_id', 'Alice', 'Wonderland');
$report = new HumanResourceReport($person);

var_dump($report->getFullName()); // string(16) "Alice Wonderland"
var_dump($report->getReportIdentifier()); // string(49) "HR_REPORT_ID_uniq_id"

3. Exemple sans l'usage de friend via des getters spécifiques

Voici l'idée : les getters de la classe Person vérifient que la classe appellante est bien la seule à avoir le droit d'accéder aux données.
La difficulté ici est de savoir quelle est la classe appelante et en PHP, une (seule?) façon de faire est d'utiliser la fonction debug_backtrace qui permet de savoir le cheminement entre les différents appels de fonctions.

<?php
class Person
{
    protected $id;
    protected $firstName;
    protected $lastName;

    public function __construct($id, $firstName, $lastName)
    {
        $this->id = $id;
        $this->firstName = $firstName;
        $this->lastName = $lastName;
    }

    public function mustBeGranted()
    {
        $trace = debug_backtrace();

        if (isset($trace[2]['class']) && HumanResourceReport::class === $trace[2]['class']) {
            return true;
        }

        throw new \Exception("The caller class is not allowed to do this.");
    }

    public function getId()
    {
        $this->mustBeGranted();

        return $this->id;
    }

    public function getFirstName()
    {
        $this->mustBeGranted();

        return $this->firstName;
    }

    public function getLastName()
    {
        $this->mustBeGranted();

        return $this->lastName;
    }
}

class HumanResourceReport
{
    private $person;

    public function __construct(Person $person)
    {
        $this->person = $person;
    }

    public function getFullName()
    {
        return $this->person->getFirstName() . ' ' . $this->person->getLastName();
    }

    public function getReportIdentifier()
    {
        return "HR_REPORT_ID_{$this->person->getId()}";
    }
}

$person = new Person('uniq_id', 'Alice', 'Wonderland');
$report = new HumanResourceReport($person);

var_dump($report->getFullName()); // string(16) "Alice Wonderland"
var_dump($report->getReportIdentifier()); // string(49) "HR_REPORT_ID_uniq_id"
var_dump($person->getFirstName()); // PHP Fatal error:  Uncaught Exception: The caller class is not allowed to do this.

3.1 Pourquoi retourner une exception ?

1. Parce que c'est mieux que de faire un trigger_error puisqu'on peut le catcher de façon plus élégante.
2. Il faudrait définir une exception spécifique.

3.2 Pourquoi l'index 2 ?

Ici on utilise l'index 2 du debug_backtrace car dans le cas où on définirait une méthode intermédiaire mustBeGranted :
index 0 : on aurait class: Person, function: mustBeGranted
index 1 : on aurait class: Person, function: getId (ou l'une des deux autres)
index 2 : on aurait class: HumanResourceReport, function getFullName (ou getReportIdentifier)
Je ne recommande pas forcément ce genre de code pour deux raisons :
1. L'usage de debug_backtrace ne devrait jamais être utilisé ailleurs qu'en développement/test
2. La classe Person ne devrait pas avoir de dépendance avec la classe HumanResourceReport (c'est d'ailleurs aussi pour ça que je suis contre l'idée du 'trait' friend).